#!/usr/bin/python3
# -*- coding: utf-8 -*-

from logging import getLogger
from os.path import isfile
from collections import Iterable

from yaml import safe_load

from .credentials import Credential, PasswordCredential, KeyPairCredential
from .entities import Project, Node, Job
from .validators import reject_if_blank, reject_if_not_type

_LOGGER = getLogger(__name__)

_CREDENTIALS: dict = dict()
_JOBS: dict = dict()


def parse_sequence(s: (str, Iterable)) -> (tuple, None):
    """解析序列。
    待解析的对象可以是字符串，或者元组或者列表。
    如果是字符串，那么按照逗号分隔为序列，每个元素去掉首尾空格，不包含空字符串。
    如果是列表则转化为元组。
    如果是元组则原样返回。
    其它情况返回 `None` 。

    :param s: 待解析的序列。
    :return: 解析结果。
    """
    if not s:
        return None

    if isinstance(s, str):
        result = tuple(item.strip() for item in s.split(',') if item and not item.isspace())
    elif isinstance(s, Iterable):
        result = tuple(str(item) for item in s if item is not None)
    else:
        return None

    return result if result else None


def find_credential(name: str) -> Credential:
    """根据身份凭据的名字获取凭据，如果找不到指定名字的凭据则返回 `None` 。"""
    reject_if_blank(name)

    return _CREDENTIALS.get(name.strip())


def find_job(name: str) -> Job:
    """根据任务的名字获取任务，如果找不到指定名字的任务则返回 `None` 。"""
    reject_if_blank(name)

    return _JOBS.get(name.strip(), None)


def load_all_dummy() -> None:
    """模拟加载所有的项目、任务和节点配置。"""
    global _JOBS

    _JOBS.clear()

    # _PROJECTS.extend([
    #     Project('accsrv-bin', 'binary of accsrv', status=PRJ_SUCCESSFUL,
    #             last_timestamp=local_datetime(2018, 7, 28, 13, 24, 55),
    #             last_duration=timedelta(minutes=1, seconds=20),
    #             jobs={'deploy': None, 'restore': None, 'archive': None}, nodes=('test1', 'test2',)),
    #     Project('accsrv-resources', 'resources of accsrv', status=PRJ_SUCCESSFUL,
    #             last_timestamp=local_datetime(2018, 3, 24, 9, 15, 32), last_duration=timedelta(seconds=32),
    #             jobs={'deploy': None, 'restore': None}, nodes=('test1', 'test2',)),
    #     Project('p2psrv-bin', 'binary of p2psrv', status=PRJ_UNDEPLOYED, enabled=False),
    #     Project('p2psrv-resources', 'resources of p2psrv', status=PRJ_FAILED, enabled=False,
    #             last_timestamp=local_datetime(2018, 4, 1, 17, 29, 1), last_duration=timedelta(seconds=24),
    #             jobs={'deploy': None, 'restore': None}, nodes='test1'),
    # ])
    # _NODES.extend([
    #     Node('test1', 'test server', hostname='192.168.11.30', bundle='test', props={
    #         'ssh-port': 22,
    #         'credential': 'ci1'
    #     }),
    #     Node('test2', 'test server', hostname='192.168.11.32', bundle='test', enabled=False, props={
    #         'ssh-port': 23,
    #         'credential': 'ci2'
    #     }),
    #     Node('pre-prd', 'beta server', hostname='192.168.11.14', bundle='prd'),
    # ])
    _JOBS.update({
        'deploy1': Job(type_='deploy', description='获取最新的软件包并发布到目标节点。',
                       params=[],
                       commands=(None, None, None,)
                       ),
        'restore1': Job(type_='restore', description='选择某个版本的已归档软件包并发布到目标节点。',
                        params=[],
                        commands=(None, None,)
                        ),
        'archive1': Job(type_='archive', description='将当前工作区的软件包归档。',
                        params=[],
                        commands=(None, None,)
                        ),
    })


def load_all(
        credentials_filename: str,
        jobs_filename: str
) -> bool:
    """从配置文件中加载身份凭据和任务配置。

    :param credentials_filename: 包含了所有身份验证凭据的文件。
    :param jobs_filename: 包含了所有任务定义的文件。
    """
    reject_if_blank(credentials_filename)
    reject_if_blank(jobs_filename)

    global _CREDENTIALS, _JOBS

    _CREDENTIALS.clear()
    _JOBS.clear()

    if not isfile(credentials_filename):
        _LOGGER.warning('Cannot open credentials file %s for read', credentials_filename)
        return False

    if not isfile(jobs_filename):
        _LOGGER.warning('Cannot open jobs file %s for read', jobs_filename)
        return False

    with open(credentials_filename, mode='rt', encoding='utf-8') as f:
        _LOGGER.debug('Loading credentials from %s', credentials_filename)
        doc = safe_load(f)
        credentials = dict(load_all_credentials(doc))
        _CREDENTIALS.update(credentials)
        _LOGGER.debug('%d credential(s) loaded', len(credentials))

    with open(jobs_filename, mode='rt', encoding='utf-8') as f:
        _LOGGER.debug('Loading jobs from %s', jobs_filename)
        doc = safe_load(f)
        jobs = dict(load_all_jobs(doc))
        _JOBS.update(jobs)
        _LOGGER.debug('%d job(s) loaded', len(jobs))

    return True


def load_all_data(filename: str) -> tuple:
    """从配置文件中加载所有的项目和节点配置。

    :param filename: 包含了项目和节点信息的配置文件。
    :return:
    """
    if not isfile(filename):
        _LOGGER.warning('cannot open metadata file %s for read', filename)

    with open(filename, mode='rt', encoding='utf-8') as f:
        _LOGGER.debug('Loading projects and nodes from %s', filename)
        doc = safe_load(f)
        projects = list(load_all_projects(doc.get('projects', None)))
        nodes = list(load_all_nodes(doc.get('nodes', None)))
        _LOGGER.debug('%d project(s) and %d node(s) loaded', len(projects), len(nodes))

        return projects, nodes


def load_all_credentials(
        items: dict
) -> Iterable:
    if not items:
        raise StopIteration()

    for key, value in items.items():
        key = key.strip() if key else ''
        if not key:
            _LOGGER.warning('Illegal credential name: %s', key)
            continue
        if not isinstance(value, dict):
            _LOGGER.warning('Illegal credential: %s', repr(value))

        if 'username' not in value:
            _LOGGER.warning('Illegal credential: %s', value)
            continue

        if 'password' in value:
            yield key, PasswordCredential(value['username'], value['password'])
        elif 'pkey' in value:
            yield key, KeyPairCredential(value['username'], value['pkey'])
        else:
            _LOGGER.warning('unknown credential: %s', value)
            continue


def load_all_jobs(
        items: dict
) -> Iterable:
    if not items:
        raise StopIteration()

    for key, value in items.items():
        key = key.strip() if key else ''
        if not key:
            _LOGGER.warning('Illegal job name: %s', key)
            continue
        if not isinstance(value, dict):
            _LOGGER.warning('Illegal job: %s', repr(value))
            continue

        type_ = value.pop('type', '').strip()
        if not type_:
            _LOGGER.warning('Illegal job.type: %s', type_)
            continue
        value['type_'] = type_

        commands = value.pop('commands', None)
        if not isinstance(commands, (list, tuple)):
            _LOGGER.warning('Illegal job.commands: %s', repr(commands))
            continue

        yield key, Job(commands=(load_command(item) for item in commands), **value)


_CREDENTIAL_KEY = 'credential'


def load_command(
        items: dict
) -> dict:
    reject_if_not_type(items, dict)

    if _CREDENTIAL_KEY in items:
        credential_name = items[_CREDENTIAL_KEY]
        if isinstance(credential_name, str):
            credential_name = credential_name.strip()
            credential = find_credential(credential_name) if credential_name else None
            if credential:
                items[_CREDENTIAL_KEY] = credential
            else:
                _LOGGER.warning('Cannot find credential: %s', repr(credential_name))
                del items[_CREDENTIAL_KEY]
        else:
            _LOGGER.warning('Illegal credential: %s', repr(credential_name))
            del items[_CREDENTIAL_KEY]

    return items


def load_all_projects(
        items: Iterable
):
    if not items:
        raise StopIteration()

    for item in items:
        if item:
            jobs = item.pop('jobs', None)
            jobs = parse_sequence(jobs)
            if jobs:
                item['jobs'] = jobs
            nodes = item.pop('nodes', None)
            nodes = parse_sequence(nodes)
            if nodes:
                item['nodes'] = nodes
            yield Project(**item)


def load_all_nodes(
        items: Iterable
):
    if not items:
        raise StopIteration()

    for item in items:
        if item:
            name = item.pop('name')
            description = item.pop('description')
            bundle = item.pop('bundle')
            hostname = item.pop('hostname')
            enabled = item.pop('enabled', True)
            yield Node(name=name, description=description, hostname=hostname, bundle=bundle, enabled=enabled,
                       props=dict(item))
